//
// Created by Gorun on 2022/5/2.
//

#ifndef LOXPP_SCANNER_H
#define LOXPP_SCANNER_H

#include <iostream>
#include <map>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "Error.h"
#include "Token.h"

using namespace std;

class Scanner {
    static const map<string, TokenType> keywords;
    string_view source;
    vector<Token> tokens;
    int start = 0;
    int current = 0;
    int line = 1;
    int column = 0;

public:
    explicit Scanner(string_view source)
    :source{source}
    {}

    vector<Token> scanTokens() {
        while (!isAtEnd()) {
            start = current;
            scanToken();
        }

        tokens.emplace_back(END, "", nullptr, line, column);
        return tokens;
    }

private:
    void scanToken() {
        char c = advance();
        switch (c) {
            case '?': addToken(QUESTION); break;
            case ':': addToken(COLON); break;
            case '(': addToken(LEFT_PAREN); break;
            case ')': addToken(RIGHT_PAREN); break;
            case '{': addToken(LEFT_BRACE); break;
            case '}': addToken(RIGHT_BRACE); break;
            case ',': addToken(COMMA); break;
            case '.': addToken(DOT); break;
            case '-': addToken(MINUS); break;
            case '+': addToken(PLUS); break;
            case ';': addToken(SEMICOLON); break;
            case '*': addToken(STAR); break;
            case '!': addToken(match('=') ? BANG_EQUAL : BANG); break;
            case '=': addToken(match('=') ? EQUAL_EQUAL : EQUAL); break;
            case '<': addToken(match('=') ? LESS_EQUAL : LESS); break;
            case '>': addToken(match('=') ? GREATER_EQUAL : GREATER); break;
            case '/':
                if (match('/')) {
                    while (peek() != '\n' && !isAtEnd())
                        advance();
                } else {
                    addToken(SLASH);
                }
                break;
            case ' ': case '\r': case '\t': break; //Blank ignored
            case '\n': line++; column=0; break;
            case '"': string(); break;
            default:
                if (isNumberic(c)) {
                    number();
                } else if (isAlpha(c)) {
                    identifier();
                } else {
                    error(line, column, "Unexpected character.");
                }
                break;
        }
    }

    void addToken(TokenType type) {
        addToken(type, nullptr);
    }

    void addToken(TokenType type, any literal) {
        //Lexeme have everything about the token in the source code
        //There's a member function called string at this scope, too
        //Thus use std::string to express the meaning
        std::string lexeme{source.substr(start, current - start)};
        tokens.emplace_back(type, std::move(lexeme), std::move(literal),
                            line, column);
    }

    void identifier() { //Identifiers have no literal
        while (isAlphaNumberic(peek()))
            advance();

        std::string text{source.substr(start, current-start)};

        TokenType type;
        auto match = keywords.find(text);
        if (match==keywords.end()) {
            //This is not a keyword
            type = IDENTIFIER;
        } else {
            type = match->second;
        }

        addToken(type);
    }

    void number() { //Numbers have literal of itself
        while (isNumberic(peek()))
            advance();
        if (peek()=='.'&&isNumberic(peekNext())) {
            //there's a float part;
            advance();
            while(isNumberic(peek()))
                advance();
        }

        addToken(NUMBER,
                 stod(std::string{source.substr(start, current-start)}));
    }

    void string() { //String have literal of itself without quotes
        while (peek()!='"'&&!isAtEnd()) {
            if (peek()=='\n') ++line; // enable ex-line strings.
            advance();
        }

        if (isAtEnd()) {
            error(line, column, "Unterminated string.");
        }

        advance();

        std::string value{source.substr(start+1, current-start-2)}; //strip the quotes.
        addToken(STRING, value);
    }

    bool match(char expected) {
        if (isAtEnd()) return false;
        if (source[current]!=expected) return false;
        ++current; column++;
        return true;
    }

    char peek() {
        if (isAtEnd()) return '\0';
        return source[current];
    }

    char peekNext() {
        if (current+1>=source.length()) return '\0';
        return source[current+1];
    }

    static bool isAlpha(char ch) {
        return ('a'<=ch&&ch<='z')||('A'<=ch&&ch<='Z');
    }

    static bool isNumberic(char ch) {
        return ('0'<=ch&&ch<='9');
    }

    static bool isAlphaNumberic(char ch) {
        return isAlpha(ch) || isNumberic(ch);
    }

    char advance() {
        column++;
        return source[current++];
    }

    bool isAtEnd() {
        return current>=source.length();
    }

};

const map<string, TokenType> Scanner::keywords = {
        {"and", AND},
        {"class", CLASS},
        {"else", ELSE},
        {"false", FALSE},
        {"for", FOR},
        {"fun", FUN},
        {"if", IF},
        {"nil", NIL},
        {"or", OR},
        {"print", PRINT},
        {"println", PRINTLN},
        {"input", INPUT},
        {"return", RETURN},
        {"super", SUPER},
        {"this", THIS},
        {"true", TRUE},
        {"var", VAR},
        {"while", WHILE}
};

#endif //LOXPP_SCANNER_H
